package com.ruoyi.framework.shiro.service;

import java.util.List;
import java.util.Set;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;

import com.google.common.base.Charsets;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
import com.ruoyi.common.exception.user.*;
import com.ruoyi.framework.config.properties.LoginErrorProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import com.ruoyi.common.constant.Constants;
import com.ruoyi.common.constant.ShiroConstants;
import com.ruoyi.common.constant.UserConstants;
import com.ruoyi.common.core.domain.entity.SysRole;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.enums.UserStatus;
import com.ruoyi.common.utils.DateUtils;
import com.ruoyi.common.utils.IpUtils;
import com.ruoyi.common.utils.MessageUtils;
import com.ruoyi.common.utils.ServletUtils;
import com.ruoyi.common.utils.ShiroUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.system.service.ISysConfigService;
import com.ruoyi.system.service.ISysMenuService;
import com.ruoyi.system.service.ISysUserService;

/**
 * 登录校验方法
 *
 * @author ruoyi
 */
@Component
public class SysLoginService {
    @Autowired
    private SysPasswordService passwordService;

    @Autowired
    private ISysUserService userService;

    @Autowired
    private ISysMenuService menuService;

    @Autowired
    private ISysConfigService configService;

    /**
     * 访问 Redis
     */
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 登录错误配置
     */
    @Autowired
    private LoginErrorProperties loginErrorProperties;

    /**
     * 布隆过滤器
     * expectedInsertions 预期插入值
     * 这个值的设置相当重要，如果设置的过小很容易导致饱和而导致误报率急剧上升，如果设置的过大，也会对内存造成浪费，所以要根据实际情况来定
     * fpp                误差率，例如：0.01,表示误差率为1%
     */
    private static BloomFilter<String> ipFilter = null;

    /**
     * 创建一个新的布隆过滤器
     */
    private static void createFilter() {
        synchronized (SysLoginService.ipFilter) {
            SysLoginService.ipFilter = BloomFilter.create(Funnels.stringFunnel(Charsets.UTF_8), 100_0000, 0.01);
        }
    }

    /**
     * 创建一个新的布隆过滤器
     */
    static {
        SysLoginService.createFilter();
    }

    /**
     * 登录
     */
    public SysUser login(String username, String password) {
        // 验证码校验
        if (ShiroConstants.CAPTCHA_ERROR.equals(ServletUtils.getRequest().getAttribute(ShiroConstants.CURRENT_CAPTCHA))) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.jcaptcha.error")));
            throw new CaptchaException();
        }
        // 用户名或密码为空 错误
        if (StringUtils.isEmpty(username) || StringUtils.isEmpty(password)) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("not.null")));
            throw new UserNotExistsException();
        }
        // 密码如果不在指定范围内 错误
        if (password.length() < UserConstants.PASSWORD_MIN_LENGTH
                || password.length() > UserConstants.PASSWORD_MAX_LENGTH) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
            throw new UserPasswordNotMatchException();
        }

        // 用户名不在指定范围内 错误
        if (username.length() < UserConstants.USERNAME_MIN_LENGTH
                || username.length() > UserConstants.USERNAME_MAX_LENGTH) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.not.match")));
            throw new UserPasswordNotMatchException();
        }

        // IP黑名单校验
        String blackStr = configService.selectConfigByKey("sys.login.blackIPList");
        if (IpUtils.isMatchedIp(blackStr, ShiroUtils.getIp())) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("login.blocked")));
            throw new BlackListException();
        }

        // 查询用户信息
        SysUser user = userService.selectUserByLoginName(username);

        /**
         if (user == null && maybeMobilePhoneNumber(username))
         {
         user = userService.selectUserByPhoneNumber(username);
         }

         if (user == null && maybeEmail(username))
         {
         user = userService.selectUserByEmail(username);
         }
         */

        if (user == null) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.not.exists")));
            throw new UserNotExistsException();
        }

        if (UserStatus.DELETED.getCode().equals(user.getDelFlag())) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.password.delete")));
            throw new UserDeleteException();
        }

        if (UserStatus.DISABLE.getCode().equals(user.getStatus())) {
            AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_FAIL, MessageUtils.message("user.blocked", user.getRemark())));
            throw new UserBlockedException();
        }

        passwordService.validate(user, password);

        AsyncManager.me().execute(AsyncFactory.recordLogininfor(username, Constants.LOGIN_SUCCESS, MessageUtils.message("user.login.success")));
        setRolePermission(user);
        recordLoginInfo(user.getUserId());
        return user;
    }

    /**
     private boolean maybeEmail(String username)
     {
     if (!username.matches(UserConstants.EMAIL_PATTERN))
     {
     return false;
     }
     return true;
     }

     private boolean maybeMobilePhoneNumber(String username)
     {
     if (!username.matches(UserConstants.MOBILE_PHONE_NUMBER_PATTERN))
     {
     return false;
     }
     return true;
     }
     */

    /**
     * 设置角色权限
     *
     * @param user 用户信息
     */
    public void setRolePermission(SysUser user) {
        List<SysRole> roles = user.getRoles();
        if (!roles.isEmpty() && roles.size() > 1) {
            // 多角色设置permissions属性，以便数据权限匹配权限
            for (SysRole role : roles) {
                Set<String> rolePerms = menuService.selectPermsByRoleId(role.getRoleId());
                role.setPermissions(rolePerms);
            }
        }
    }

    /**
     * 记录登录信息
     *
     * @param userId 用户ID
     */
    public void recordLoginInfo(Long userId) {
        SysUser user = new SysUser();
        user.setUserId(userId);
        user.setLoginIp(ShiroUtils.getIp());
        user.setLoginDate(DateUtils.getNowDate());
        userService.updateUserInfo(user);
    }

    /**
     * 产生缓存用户登录错误次数的 key
     *
     * @return key
     */
    private String generateErrorCountKey(Object loginName) {
        return this.loginErrorProperties.getCountKeyPrefix() + ":" + ShiroUtils.getIp() + ":" + loginName;
    }

    /**
     * 判断该 ip 是否允许以该账户登录
     * 登录失败达到一定次数则禁止登录
     *
     * @param loginName 登录名称
     * @throws UserPasswordRetryLimitExceedException
     */
    public void allowLogin(Object loginName) {
        if (!SysLoginService.ipFilter.mightContain(ShiroUtils.getIp())) {//错误次数肯定没有达到上线，允许登录
            if (ThreadLocalRandom.current().nextInt(100) == 10) {//重建布隆过滤器
                SysLoginService.createFilter();
            }

            return;
        }

        //key： 前缀加ip
        String key = this.generateErrorCountKey(loginName);
        Object value = this.redisTemplate.opsForValue().get(key);

        Integer errorCount = 0;
        if (value instanceof Integer) {
            errorCount = (Integer) value;
        } else {
            return;
        }

        if (errorCount != null && errorCount >= this.loginErrorProperties.getMaxCount()) {//登录错误的次数太多
            throw new UserPasswordRetryLimitExceedException(this.loginErrorProperties.getMaxCount().intValue());
        }
    }

    /**
     * 登录错误数+1
     *
     * @param loginName 登录名称
     * @return 目前登录错误了几次
     */
    public Integer countError(Object loginName) {
        //在 Redis 中记录登录次数
        //key： 前缀加ip
        String key = this.generateErrorCountKey(loginName);

        //如果 Redis 中没有记录该 ip 的登录失败次数，则记录
        Boolean keyIsAbsent = this.redisTemplate.opsForValue().setIfAbsent(key, 1, this.loginErrorProperties.getExpiration(), TimeUnit.MINUTES);

        if (keyIsAbsent) {//刚记录的，基本不可能超出最大可尝试的次数
            return 1;
        }

        //次数自增
        Integer errorCount = redisTemplate.opsForValue().increment(key).intValue();

        if (errorCount != null && errorCount >= this.loginErrorProperties.getMaxCount()) {//登录错误的次数太多
            synchronized (SysLoginService.ipFilter) {
                SysLoginService.ipFilter.put(ShiroUtils.getIp());
            }
        }

        return errorCount;
    }

}
